Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor adjustments to profiler #11376

Merged
merged 15 commits into from
Nov 21, 2023
Merged

Minor adjustments to profiler #11376

merged 15 commits into from
Nov 21, 2023

Conversation

jerelmiller
Copy link
Member

Makes some adjustments to the profile and profileHook utilities for our tests to better align with the paradigm of those utilities.

One of the points of confusion I first had when using this utility is the point in time in which each of the profiled component helper functions execute, specifically the currentRenderCount, getCurrentRender, and takeUntilRenderCount methods. These methods operated on the "real" point in time, while the other methods (takeRender, peekRender, etc.) all relied on the iteratorPosition to determine where in the render cycle we were.

This confusion led me to use/abuse the API in #11300 that strayed from the original intent of the profiler utility.

To move this utility more in the direction of its original intent, I'm making some updates to the existing API to further clarify this behavior, specifically these changes:

  • Rename currentRenderCount to totalRenderCount.

This makes it clear that we are trying to measure the total number of times the profiled component has rendered up to a given point in the test, regardless of iterator position.

  • Modify the behavior of getCurrentRender to return the render at the current iterator position.

With this change, "current" is meant to represent the render at the current iterator position, not the latest render in the array.

  • Removed the takeUntilRenderCount method.

For starters, this wasn't currently used anywhere. Secondly, the implementation of this method relied on the total number of renders rather than the iterator count, so trying to use this in the test behaved unpredictably. While I could fix this, I want to see if simply using await Profiled.takeRender() the number of times we want to advance is sufficient enough for our tests.

Going forward, I propose that if we need a utility that gets us information about the latest render, we use the term "latest" in the name. This makes it clearer that a method with "current" in the name is meant to represent the "render at the current iterator position". I'm holding off on doing that in this PR though as I'd like to try and force us to use methods that rely on the iterator as much as possible to try and avoid abuse of the API. We can revisit in the future to determine if we need this.


One other adjustment I've made to snapshots is that I've introduced a setSnapshot method that takes a partial snapshot and does a shallow merge with the full snapshot. Think of this akin to React's old this.setState method in class components where it would shallow merge the object with the current state. I found this to be a bit more ergonomic in cases where I'd just like to update the value of a particular key in the snapshot but don't have to pass a function to do so.

@jerelmiller jerelmiller requested a review from phryneas November 21, 2023 00:09
Copy link

changeset-bot bot commented Nov 21, 2023

⚠️ No Changeset found

Latest commit: 874acf6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

github-actions bot commented Nov 21, 2023

size-limit report 📦

Path Size
dist/apollo-client.min.cjs 37.08 KB (0%)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs" 43.51 KB (0%)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs" (production) 42.01 KB (0%)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js" 32.61 KB (0%)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js" (production) 31.27 KB (0%)
import { ApolloProvider } from "dist/react/index.js" 1.22 KB (0%)
import { ApolloProvider } from "dist/react/index.js" (production) 1.21 KB (0%)
import { useQuery } from "dist/react/index.js" 4.28 KB (0%)
import { useQuery } from "dist/react/index.js" (production) 4.1 KB (0%)
import { useLazyQuery } from "dist/react/index.js" 4.59 KB (0%)
import { useLazyQuery } from "dist/react/index.js" (production) 4.41 KB (0%)
import { useMutation } from "dist/react/index.js" 2.55 KB (0%)
import { useMutation } from "dist/react/index.js" (production) 2.54 KB (0%)
import { useSubscription } from "dist/react/index.js" 2.24 KB (0%)
import { useSubscription } from "dist/react/index.js" (production) 2.2 KB (0%)
import { useSuspenseQuery } from "dist/react/index.js" 4.62 KB (0%)
import { useSuspenseQuery } from "dist/react/index.js" (production) 4.05 KB (0%)
import { useBackgroundQuery } from "dist/react/index.js" 4.14 KB (0%)
import { useBackgroundQuery } from "dist/react/index.js" (production) 3.56 KB (0%)
import { useReadQuery } from "dist/react/index.js" 3 KB (0%)
import { useReadQuery } from "dist/react/index.js" (production) 2.94 KB (0%)
import { useFragment } from "dist/react/index.js" 2.09 KB (0%)
import { useFragment } from "dist/react/index.js" (production) 2.04 KB (0%)

/**
* Iterates the renders until the render count is reached.
*/
takeUntilRenderCount(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behaved in a way that was unintuitive due to the fact that it used the total render count rather than the iterator in its implementation. This isn't currently used, so I'd like us to try using the other methods first before determining if we need to reintroduce this.

If we do reintroduce, I'd propose that instead of thinking about taking each render up to a specific count, that instead this method advances the iterator a certain number of renders from "current". Doing so would prevent weird situations like this:

await Profiled.takeRender()
await Profiled.takeRender()
await Profiled.takeRender()

// This makes no sense since render #2 is now in the past
await Profiled.takeUntilRenderCount(2)

*/
currentRenderCount(): number;
totalRenderCount(): number;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now the only function that doesn't rely on the iterator position in its implementation. If possible, we should keep it that way to try and maintain the intent of the original API which should force you to iterate on each render in your tests.


interface ProfiledComponentOnlyFields<Props, Snapshot> {
// Allows for partial updating of the snapshot by shallow merging the results
setSnapshot: SetSnapshot<Snapshot>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a setSnapshot method which allows us now to set a partial snapshot, which gets merged with the full snapshot. I took the name from React's old this.setState() method in class components, which has similar behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the comment above - we might also consider the current behaviour, but immer-wrapped.

// once before we can get a current render.
const currentPosition = iteratorPosition - 1;

if (currentPosition < 0) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I'm changing "current render" to mean the render pointing to the most recent takeRender call, I'm requiring that takeRender be called at least once. We could just point at the first render if its available, but I felt that to be weird in the following situation:

const current1 = Profiled.getCurrentRender();
const result = await Profiled.takeRender();

// Without requiring `takeRender`, this would be equal to current1
const current2 = Profiled.getCurrentRender();

On the flip side, we could make getCurrentRender be equal to the iterator position, but this means that it represents the render "after" the last takeRender call, which also feels weird. This means that calling getCurrentRender after you've exhausted the render array via takeRender would point to a render that does not exist.

Comment on lines 505 to 507
ProfiledApp.setSnapshot(({ errorCount, errors }) => ({
errorCount: errorCount + 1,
errors: errors.concat(error),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm almost considering that we get immer in here for the tests.

That would end up looking like this,

Suggested change
ProfiledApp.setSnapshot(({ errorCount, errors }) => ({
errorCount: errorCount + 1,
errors: errors.concat(error),
ProfiledApp.updateSnapshot(snapshot => {
snapshot.errorCount++;
snapshot.errors.push(error)

but would still enable something like this

            ProfiledApp.updateSnapshot(snapshot => {
              return {}
            })

which would reset the snapshot to an empty object - which would not be possible with the setSnapshot notation here.

Copy link
Member Author

@jerelmiller jerelmiller Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm indifferent here. I didn't consider an alternative library only because it was a small change, but I can see how it would be useful.

FWIW, to reset back to an empty object, I figured you'd just use updateSnapshot directly since updateSnapshot does a whole replacement on the snapshot object.

ProfiledApp.updateSnapshot({})

I never intended setSnapshot to be used for this case, and IMO was the differentiator between the update* and set* versions of this function.

The reason I felt like a setSnapshot would be useful (which you don't see in this PR since this PR contains updates to counts, but would be used in useLoadableQuery) is cases where you want to record the value from a hook, which might only be part of the whole snapshot.

function Child({ queryRef }) {
  const { data } = useReadQuery(queryRef)

  // No need for the function callback since I can just update the partial snapshot
  ProfiledApp.setSnapshot({ data })
}

// or record the whole object under a specific key without the need for a callback function
ProfiledApp.setSnapshot({ result: useReadQuery(queryRef) })

Again, I'm indifferent, and if adding a dependency on immer would make this more useful, thats fine.

Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'm just now realizing that produce in Immer enforces a callback function.

The motivation behind setSnapshot was to try and avoid the noise of a callback function when I just wanted to record a new value for a particular key. This PR doesn't do a great job of highlighting this use case since updateSnapshot is mainly used to increment counters, but there are a decent number of cases in the useLoadableQuery PR that would benefit from the setSnapshot style.

const result = useReadQuery(queryRef)

ProfiledApp.setSnapshot({ result })

// vs
ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result }))

// or with immer
ProfiledApp.updateSnapshot((snapshot) => {
  snapshot.result = result;
})

This is especially useful if you just want to inline everything:

ProfiledApp.setSnapshot({ result: useReadQuery(queryRef) })

I'd like to preserve some way of being able to partial update a key on the snapshot without the use of a callback function if possible to cut down on the noise. I do agree that the immer-style way of doing things is useful for incrementing counters or "mutating" existing values. Perhaps there is a way we can have both?


interface ProfiledComponentOnlyFields<Props, Snapshot> {
// Allows for partial updating of the snapshot by shallow merging the results
setSnapshot: SetSnapshot<Snapshot>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the comment above - we might also consider the current behaviour, but immer-wrapped.

@jerelmiller
Copy link
Member Author

Got a verbal approval from @phryneas, so this is good to merge once tests pass.

@jerelmiller jerelmiller merged commit bb17509 into main Nov 21, 2023
25 checks passed
@jerelmiller jerelmiller deleted the jerel/updates-to-profiler branch November 21, 2023 19:17
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 22, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants